/**
 * Collaborative Story Development Web Application (CSDApp) Copyright Anton
 * Strack 2014
 *
 * This file is part of Collaborative Story Development Application (CSDApp).
 *
 * CSDApp is free software: you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or any later version.
 *
 * CSDApp is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * CSDApp. If not, see <http://www.gnu.org/licenses/>.
 *
 * TESTING ACCOUNTS AND PASSWORDS Writer:pass2 Admin:pass1
 */
package csdwa;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.sql.Connection;
import java.util.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

public class Authenticator {

    private String baseSalt = "4NdcFc21f$545fetC!8a3c@5b64";// salt not stored in a database but maybe stored in a xml configuration file eventually
    private Date date;
    private Connection conn = null;
    private PreparedStatement ps = null;
    private String getSalt = "SELECT PasswordSalt FROM useraccount WHERE LOWER(Username)=? LIMIT 1";
    private String check = "SELECT ID, RoleID, Email, LoginDate, LastLoginDate FROM useraccount WHERE LOWER(Username)=? AND Password=? LIMIT 1";

    public Authenticator() {
    }

    public Authenticator(Connection conn) {
        this.conn = conn;
    }

    /**
     * This method authenticates the user according to their session.
     *
     * @param request the request object
     * @return a user with a role of the user as determined by the
     * authorization. If no password or proper session could be found for the
     * user, then the returned user is a guest user.
     */
    public User authenticate(HttpServletRequest request) {
        HttpSession session = request.getSession();
        User user = null;
        if (session == null || session.isNew() || session.getAttribute("user") == null) {
            // if salt wasn't found for username or username+password combo not found in database, then set user as limited rights guest.
            if (user == null) {
                user = new User();
                user = this.setUserGuest(user);
            }
        } else {
            user = (User) session.getAttribute("user");
            if (user.getRoleId() <= 0 || user.getRoleId() == InfoAggregator.getInstance().getRoleTypeId("guest")) {
                session.invalidate();
                this.authenticate(request);// recall method and thus get guest user most likely
            } else {
                this.setSession(session, user);
                request.setAttribute("loggedIn", 1);
            }
        }
        return user;
    }

    /**
     * This method authenticates the user by a user name and password.
     *
     * @param username the name of the user used to login
     * @param password the password of the user
     * @param request the request object.
     * @return a user with a role determined by the authorization. If no
     * password or proper session could be found for the user, then the returned
     * user is a guest user.
     */
    public User authenticateLogin(String username, String password, HttpServletRequest request) {
        User user = new User();
        user = this.setUserGuest(user);
        if (!username.isEmpty() && !password.isEmpty()) {
            user.setName(username);
            user.setPassword(password);
            String salt = getSalt(user);
            if (salt.isEmpty() == false) {
                user.setSalt(salt);
                user = checkUser(user);
                if (user.getRoleId() != InfoAggregator.getInstance().getRoleTypeId("guest")) {
                    this.setSession(request.getSession(), user);
                    request.setAttribute("loggedIn", 1);
                }
            }
        }
        return user;
    }

    private void setSession(HttpSession session, User user) {
        session.setAttribute("user", user);
        session.setMaxInactiveInterval(Configuration.getSessionMaxDuration());
        //probably should set some session id
    }

    /**
     * Get the salt value for the user as identified by their username.
     *
     * @param user the user to get the salt of
     * @return the salt of the user or an empty string if none was found.
     */
    private String getSalt(User user) {
        String salt = "";
        ResultSet rs;
        try {
            this.ps = conn.prepareStatement(this.getSalt);
            ps.setString(1, user.getName().toLowerCase());
            rs = ps.executeQuery();
            while (rs.next()) {
                salt = rs.getString("PasswordSalt");
            }
        } catch (SQLException ex) {
            Logger.getLogger(Authenticator.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                ps.close();
            } catch (SQLException ex) {
                Logger.getLogger(Authenticator.class.getName()).log(Level.SEVERE, null, ex);
            }

        }
        return salt;
    }

    /**
     * Check's the user table for the username and salted password and selects
     * the user's role accordingly.
     *
     * @param user the user with their user name, password, and salt variables
     * set.
     * @return the authenticated user or a guest user if they weren't
     * authenticated.
     */
    private User checkUser(User user) {

        ResultSet rs;
        try {
            this.ps = conn.prepareStatement(this.check);
            ps.setString(1, user.getName().toLowerCase());
            ps.setString(2, this.hashValue(user.getPassword(), this.getSalt(user.getSalt())));
            boolean userExists = false;
            rs = ps.executeQuery();
            while (rs.next()) {
                // user.setName(user.getName());
                user.setEmail(rs.getString("Email"));
                user.setId(rs.getInt("ID"));
                user.setRoleId(rs.getInt("RoleID"));
                user.setPassword("");// no need to store password in session
                user.setSalt(""); // no need to store salt in session
                user.setLoginDate(rs.getTimestamp("LoginDate"));
                user.setLastLoginDate(rs.getTimestamp("LastLoginDate"));
                userExists = true;
            }
            if (userExists == false) {
                user = this.setUserGuest(user);
            }

        } catch (SQLException ex) {
            Logger.getLogger(Authenticator.class.getName()).log(Level.SEVERE, null, ex);
        } finally {
            try {
                ps.close();
            } catch (SQLException ex) {
                Logger.getLogger(Authenticator.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        return user;
    }

    /**
     * Setup the default values for a guest user.
     *
     * @param user to be set up as a guest.
     * @return a guest user.
     */
    private User setUserGuest(User user) {
        this.date = new Date();
        Timestamp timestamp = new Timestamp(this.date.getTime());

        user.setName("Guest");
        user.setEmail("");
        user.setId(-1);
        user.setRoleId(InfoAggregator.getInstance().getRoleTypeId("guest"));// need to set to the actual role id for a guest user
        user.setPassword("");
        user.setSalt("");
        user.setLoginDate(timestamp); // just default value. not really imporant for a guest
        user.setLastLoginDate(timestamp); // just default value. not really important for a guest.
        return user;
    }

    /**
     * this method hashes a particular value with the provided salt value.
     *
     * @param value the value to hash
     * @param salt the salt to help strengthen the hash
     * @return the hashed value.
     */
    public String hashValue(String value, String salt) {

        String hashed = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(salt.getBytes("UTF-8"));
            byte[] bytes = md.digest(value.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < bytes.length; i++) {
                sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));
            }
            hashed = sb.toString();
        } catch (UnsupportedEncodingException ex) {
            Logger.getLogger(Authenticator.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(Authenticator.class.getName()).log(Level.SEVERE, null, ex);
        }
        return hashed;
    }

    /**
     * This method just builds the salt used to salt the password. It takes the
     * passed in salt value from the user's table row and hashes it with the
     * base salt value. This way, getting access to the user's table shouldn't
     * provide the full salt to build rainbow tables with.
     *
     * @param salt the salt value for the specific user.
     * @return a hashed salt value to be used as the salt for the password.
     */
    public String getSalt(String salt) {
        return this.hashValue(salt, this.baseSalt);
    }

    /**
     * might move this to user account creator area instead * this creates the
     * salt stored stored in the user's row
     *
     * @return the salt value
     * @throws java.security.NoSuchAlgorithmException
     * @throws java.security.NoSuchProviderException
     */
    public String createSalt() throws NoSuchAlgorithmException, NoSuchProviderException {
        this.date = new Date();
        SecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");
        byte[] salt = new byte[32];
        sr.nextBytes(salt);
        return salt.toString();
    }
}
